package org.calconnect.icalproofing;

/**
 *	<p>Validates iCalendar data using the iCal4j library.</p>
 *
 *	<p><em>Last checked in by: $Author: aron $ at $Date: 2008-10-27 13:42:54 -0700 (Mon, 27 Oct 2008) $ (UTC)</em></p>
 *
 *	@author Aron Roberts
 *	@version $Revision: 507 $
 */

import net.fortuna.ical4j.data.CalendarBuilder;
import net.fortuna.ical4j.data.UnfoldingReader;
import net.fortuna.ical4j.filter.Filter;
import net.fortuna.ical4j.filter.PeriodRule;
import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.Component;
import net.fortuna.ical4j.model.ComponentList;
import net.fortuna.ical4j.model.DateTime;
import net.fortuna.ical4j.model.Period;
import net.fortuna.ical4j.model.Property;
import net.fortuna.ical4j.model.PropertyList;
import net.fortuna.ical4j.model.TimeZone;
import net.fortuna.ical4j.model.TimeZoneRegistry;
import net.fortuna.ical4j.model.TimeZoneRegistryFactory;
import net.fortuna.ical4j.model.ValidationException;
import net.fortuna.ical4j.model.component.VAlarm;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.component.VFreeBusy;
import net.fortuna.ical4j.model.component.VTimeZone;
import net.fortuna.ical4j.model.parameter.FbType;
import net.fortuna.ical4j.model.property.Action;
import net.fortuna.ical4j.model.property.Attendee;
import net.fortuna.ical4j.model.property.CalScale;
import net.fortuna.ical4j.model.property.DateProperty;
import net.fortuna.ical4j.model.property.Description;
import net.fortuna.ical4j.model.property.DtStamp;
import net.fortuna.ical4j.model.property.DtStart;
import net.fortuna.ical4j.model.property.DtEnd;
import net.fortuna.ical4j.model.property.ProdId;
import net.fortuna.ical4j.model.property.Uid;
import net.fortuna.ical4j.model.property.Version;
import net.fortuna.ical4j.util.Calendars;
import net.fortuna.ical4j.util.CompatibilityHints;

import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.StringReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.text.ParseException;
import java.util.Collections;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;

import org.restlet.data.Status;
import org.restlet.resource.ResourceException;

public class ICal4jValidator {

	// Class variables

	// Get the name of this class
	final static String CLASSNAME = ICal4jValidator.class.getName();

	// Create a log4j 'logger' for this class, based on the class name
  //
  // This allows us to identify which log events are coming from the
  // methods of this class
	public static Logger logger = Logger.getLogger( CLASSNAME );

	// Create a second log4j 'logger' for handling the test harness for this class,
	//
	// This allows us to specify individually how to handle the
	// the output from running tests in main()
	private static Logger testHarnessLogger = Logger.getLogger( "testHarnessLogger" );
	
	// Constants for relaxed or strict parsing of iCalendar data.
	final static boolean RELAXED_PARSING_MODE = true;
	final static boolean STRICT_PARSING_MODE = false;
	
	// Regular expression pattern to infer that a text stream may contain a
	// VCALENDAR property, and thus may represent probable iCalendar data.
	final static Pattern VCALENDAR_PROPERTY_PATTERN =
		Pattern.compile( "^\\s*BEGIN:VCALENDAR\\s*$", Pattern.MULTILINE );
		
	// Constants to indicate whether iCalendar data is valid.
	final static boolean VALID = true;
	final static boolean NOT_VALID = false;

	// Instance variables
	
	// An internal representation of the iCalendar object.	
	private PropertyList properties = new PropertyList();
	private ComponentList components = new ComponentList();
	private Calendar ical = new Calendar( properties, components );

	// An iCalendar builder
	CalendarBuilder builder = new CalendarBuilder();
	
	/////////////////////////////////////////////////////////////////
	/**
	 *	Class constructor (no argument).
	 */
	public ICal4jValidator() {

		logger.debug( "In no-argument constructor for " + CLASSNAME + "..." );
		
	}

	/////////////////////////////////////////////////////////////////
	/**
	 *	Class constructor.
	 *
	 *	@param file		A file containing data conforming to
	 *								the iCalendar specification (RFC 2445)
	 */
	public ICal4jValidator( File file ) throws ResourceException {
	
		logger.debug( "In constructor( File ) for " + CLASSNAME + "..." );
		
		try {
			setTo( file );
		} catch ( ResourceException re ) {
			throw re;
		}

	}

	/////////////////////////////////////////////////////////////////
	/**
	 *	Class constructor.
	 *
	 *	@param string		A string containing data conforming to
	 *									the iCalendar specification (RFC 2445)
	 */
	public ICal4jValidator( String str ) throws ResourceException {
	
		logger.debug( "In constructor( String ) for " + CLASSNAME + "..." );
		
		try {
			setTo( str );
		} catch ( ResourceException re ) {
			throw re;
		}
		
	}

	/////////////////////////////////////////////////////////////////
	/**
	 *	Class constructor.
	 *
	 *	@param uri		A URL representing containing data conforming to
	 *								the iCalendar specification (RFC 2445)
	 */
	public ICal4jValidator( URL url ) throws ResourceException {
	
		logger.debug("In constructor( URL ) for " + CLASSNAME + "...");
		
		try {
			setTo( url );
		} catch ( ResourceException re ) {
			throw re;
		}
		
	}

	/////////////////////////////////////////////////////////////////
	/**
	 *	Reads an file on disk containing iCalendar data, parses it,
	 *	and stores it as an internal iCalendar object.
	 *
	 *	@param file		A file containing data conforming to
	 *								the iCalendar specification (RFC 2445).
	 */
	public void setTo( File file ) throws ResourceException {

    logger.debug( "In setTo( File ) ... in class " + CLASSNAME );
        
 		// Relax scrutiny of incoming, non-conformant iCalendar data
    setICalendarParsingMode( RELAXED_PARSING_MODE );
	
		try {
			FileInputStream stream = new FileInputStream( file );
			this.ical = builder.build( stream );
			
		} catch ( Exception e ) {	

			String msg = "";
			
			if ( e.getCause() != null ) {
				msg = e.getCause().getMessage();
			} else {
				msg = e.getMessage();
			}

			logger.warn(
				"Could not create iCalendar object from file '" 
				+ file.getName() 
				+ "': " 
				+ msg );

			throw new ResourceException( Status.CLIENT_ERROR_BAD_REQUEST, msg );
		}

		// Revert to requiring a higher level of iCalendar spec conformity
    setICalendarParsingMode( STRICT_PARSING_MODE );
		
	}

	/////////////////////////////////////////////////////////////////
	/**
	 *	Parses a string containing iCalendar data
	 *	and stores it as an internal iCalendar object.
	 *
	 *	@param str		A string containing data conforming to
	 *								the iCalendar specification (RFC 2445).
	 */
	public void setTo( String str ) throws ResourceException {

    logger.debug( "In setTo( String ) ... in class " + CLASSNAME );
        
 		// Relax scrutiny of incoming, non-conformant iCalendar data
    setICalendarParsingMode( RELAXED_PARSING_MODE );

		try {
		
			if ( StringUtils.isNotEmpty( str ) ) {
			
				StringReader reader = new StringReader( str );
  			UnfoldingReader unfoldingReader = new UnfoldingReader( reader, RELAXED_PARSING_MODE );
				this.ical = builder.build( unfoldingReader );
					
			} else {
				logger.warn( "iCalendar string was null or empty." );
			}
		
		// @todo Return a relevant exception here and in each setTo() method,
		// so that we can return parsing errors in the request.
		} catch ( Exception e ) {

			String msg = "";
			
			if ( e.getCause() != null ) {
				msg = e.getCause().getMessage();
			} else {
				msg = e.getMessage();
			}

			logger.warn( 
				"Could not create iCalendar object from String " +
				"'" +
				str +
				"': " +
				msg );
			
			throw new ResourceException( Status.CLIENT_ERROR_BAD_REQUEST, msg );
			
		}

		// Revert to requiring a higher level of iCalendar spec conformity
    setICalendarParsingMode( STRICT_PARSING_MODE );

	}
	
	/////////////////////////////////////////////////////////////////
	/**
	 *	Parses iCalendar data accessed via an HTTP URL
	 *	and stores it as an internal iCalendar object.
	 *
	 *	@param	url		An HTTP URL that is expected to return a representation
	 *								containing iCalendar data.
	 */
	public void setTo( URL url ) throws ResourceException {

    logger.debug( "In setTo( URL ) ... in class " + CLASSNAME );
        
 		// Relax scrutiny of incoming, non-conformant iCalendar data
    setICalendarParsingMode( RELAXED_PARSING_MODE );
	
		try {
		
			// Verify whether the URL is for the HTTP protocol
			//
			// @todo Later also permit the HTTPS protocol
			if (! url.getProtocol().equalsIgnoreCase( "http" ) ) {
				logger.warn( "URL '" + url.getProtocol() + "' must be for the HTTP protocol." );
				return;
			}
			
			URLConnection conn = url.openConnection();
			final int TIMEOUT_IN_MINUTES = 2;
			if ( TIMEOUT_IN_MINUTES > 0 ) {
				conn.setConnectTimeout( TIMEOUT_IN_MINUTES * 60 * 1000 );
			}
			HttpURLConnection httpconn = ( HttpURLConnection ) conn;
			// getInputStream() implicitly opens the HTTP connection
			InputStream stream = httpconn.getInputStream();
			this.ical = builder.build( stream );

		} catch ( Exception e ) {
		
			String msg = "";
			
			if ( e.getCause() != null ) {
				msg = e.getCause().getMessage();
			} else {
				msg = e.getMessage();
			}

			logger.warn( 
				"Could not create iCalendar object from URL " +
				"'" +
				url.toString() +
				"': " +
				msg );
			
			throw new ResourceException( Status.CLIENT_ERROR_BAD_REQUEST, msg );

		} 

		// Revert to requiring a higher level of iCalendar spec conformity
    setICalendarParsingMode( STRICT_PARSING_MODE );
		
	}

	/////////////////////////////////////////////////////////////////
	/**
	 *	Set the parsing mode for iCalendar data to either relaxed or strict.
	 *
	 *	@param		parseMode	True if parsing of iCalendar data should be relaxed;
	 *											false if parsing of iCalendar data should be strict.
	 */
	public void setICalendarParsingMode( boolean parseMode ) {

    logger.debug( "In setICalendarParsingMode( boolean ) ... in class " + CLASSNAME );
    
		CompatibilityHints.setHintEnabled( CompatibilityHints.KEY_RELAXED_PARSING, parseMode );
		CompatibilityHints.setHintEnabled( CompatibilityHints.KEY_RELAXED_UNFOLDING, parseMode );
		CompatibilityHints.setHintEnabled( CompatibilityHints.KEY_RELAXED_VALIDATION, parseMode );

	}
	
	/////////////////////////////////////////////////////////////////
	/**
	 *	Returns the internal iCalendar object.
	 *
	 *	@return 						The internal iCalendar object.
	 */
	public Calendar getCalendar() {

    logger.debug( "In getCalendar() ... in class " + CLASSNAME );
    
		return this.ical;
			
	}

	/////////////////////////////////////////////////////////////////
	/**
	 *	Tests whether the internal iCalendar object contains at least one
	 *	valid component. If not, it can be determined to be effectively "empty."
	 *
	 *	@return 		True if the iCalendar object does not contain at least one
	 *							valid component (and thus is "empty"); false if it
	 *							contains at least one valid component.
	 */
	public boolean isEmpty() {

    logger.debug( "In isEmpty() ... in class " + CLASSNAME );
    
		try {
			this.ical.validate();
		} catch ( ValidationException e ) {
			// Based on the exception message, we can determine if the iCalendar
			// object is empty
 	   	final String EMPTY_EXCEPTION_MSG = "Calendar must contain at least one component";
			if ( e.getMessage().equals( EMPTY_EXCEPTION_MSG ) ) {
				return true;
			// Otherwise, although the iCalendar object may not be valid,
			// we don't know definitively that it is empty.
			} else {
				return false;
			}
		}
		
		return false;
			
	}

	/////////////////////////////////////////////////////////////////
	/**
	 *	Tests whether a string appears to contain a VCALENDAR property,
	 *	and thus may represent probable iCalendar data.
	 *
	 *	@param			s		A String containing possible iCalendar data.
	 *
	 *	@return 		True if the string appears to contain a VCALENDAR property;
	 *							false if it does not.
	 */
	public static boolean containsVCalendarProperty( String s ) {

		Matcher VCalendarMatcher = VCALENDAR_PROPERTY_PATTERN.matcher( s );
		if ( VCalendarMatcher.find() ) {
			return true;
		} else {
			return false;
		}
		
	}
	
	/////////////////////////////////////////////////////////////////
	/**
	 *	Returns a string representation of the internal
	 *	iCalendar object.
	 *
	 *	@return 	A string representation of the internal iCalendar object.
	 */
	public String toString() {

    logger.debug( "In toString() ... in class " + CLASSNAME );
    
		return this.ical.toString();
			
	}

	/////////////////////////////////////////////////////////////////
	/**
	 *	Validates a VEvent object.
	 *
	 *	@param		vevent		A Vevent object.
	 *
	 *	@return		Results of the validation attempt.
	 */
	public ValidationResults validateVEvent( VEvent vevent ) {

	  logger.debug( "In validateVEvent( VEvent ) ... in class " + CLASSNAME );
	
		ValidationResults results;
		
		// Whether to validate properties within the VEvent, including
		// recursively validating properties of any contained components
		// (e.g. VAlarm components).
	  final boolean VALIDATE_VEVENT_PROPERTIES = true;

		try {
			vevent.validate( VALIDATE_VEVENT_PROPERTIES );
			results = new ValidationResults( VALID, ValidationResults.IS_VALID_MESSAGE );
		} catch ( ValidationException ve ) {
			results = new ValidationResults( NOT_VALID, ve );
		}
		
		return results;

	}
	
	/////////////////////////////////////////////////////////////////
	/**
	 *	Validates the internal iCalendar object.
	 *
	 *	@return		Results of the validation attempt.
	 */
	public ValidationResults validateICalendar() throws ValidationException {
 
	  logger.debug( "In validateICalendar( Calendar ) ... in class " + CLASSNAME );
	
		ValidationResults results;
		
		try {
			this.ical.validate();
			results = new ValidationResults( VALID, ValidationResults.IS_VALID_MESSAGE );
		} catch ( ValidationException ve ) {
			results = new ValidationResults( NOT_VALID, ve );
		}
		
		return results;

	}

	/////////////////////////////////////////////////////////////////
	/**
	 *	Validates an iCalendar string.
	 *
	 *	@param		str		A string representation of an iCalendar object
	 *
	 *	@return		True if the iCalendar object validates;
	 *						false if it does not validate.
	 */
	public ValidationResults validate( String str ) throws ResourceException {

	  logger.debug( "In validate( String ) ... in class " + CLASSNAME );

		ValidationResults results;
		
		if (! containsVCalendarProperty( str ) ) {
			throw new ResourceException( 
				Status.CLIENT_ERROR_BAD_REQUEST, 
				"The data provided does not appear to be iCalendar data" );
		}
  	
		try {

			setTo( str );
			results = validateICalendar();

		// An error occurred when trying to retrieve and validate the iCalendar data.
		// @todo See if we can safely remove the 'catch all' second catch block, below.
		} catch ( ResourceException re ) {
			throw re;
		} catch ( Exception e ) {
			Throwable t = (Throwable) e;
			throw new ResourceException( Status.CLIENT_ERROR_BAD_REQUEST, t );
		}
		
		return results;

	}

	/////////////////////////////////////////////////////////////////
	/**
	 *	Validates a Calendar object at a specified URL.
	 *
	 *	@param		url		An HTTP URL pointing to an iCalendar object.
	 *
	 *	@return		True if the iCalendar object validates;
	 *						false if it does not validate.
	 */
	public ValidationResults validate( URL url ) throws ResourceException {

	  logger.debug( "In validate( URL ) ... in class " + CLASSNAME );
	  
	  ValidationResults results;

		try {

			// @todo Instead of using iCal4j's built-in method for
			// retrieving iCalendar data from a URL, it may be more
			// desirable to retrieve this data using a technique
			// that is agnostic to any iCalendar proofing tool.
			//
			// This way, we can better isolate and modularize the code
			// which fetches data from a URL.  One option would be to
			// use the Restlet client for this purpose.
			
			// The following command currently still uses iCal4j's built-in method
			// retrieving iCalendar data from a URL.
			setTo( url );
			results = validateICalendar();
			
		// An error occurred when trying to retrieve and validate the iCalendar data.
		// @todo See if we can safely remove the 'catch all' second catch block, below.
		} catch ( ResourceException re ) {
			throw re;
		} catch ( Exception e ) {
			Throwable t = (Throwable) e;
			throw new ResourceException( Status.CLIENT_ERROR_BAD_REQUEST, t );
		}
		
		return results;

	}
	
	/////////////////////////////////////////////////////////////////

  public static void main( String[] args ) {

		// Display descriptive information about this class

  	System.out.println( CLASSNAME );
  	System.out.println( "$Revision: 507 $" );
  	System.out.println( "$Date: 2008-10-27 13:42:54 -0700 (Mon, 27 Oct 2008) $" );

	} // end of main() method
	
}
